﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;
using Pfz.Extensions;

namespace Pfz.Serialization
{
	/// <summary>
	/// A base class for creating serializer that are configurable (allow to set which items can be serialized).
	/// </summary>
	public abstract class ConfigurableSerializerBase
	{
		#region General (both Serialization and Deserialization)
			private readonly Dictionary<Type, ItemSerializerReference> _serializers = new Dictionary<Type, ItemSerializerReference>();

			private Stream _stream;
			/// <summary>
			/// Gets the stream used to serialize/deserialize. Only available during the process
			/// of serialization/deserialization.
			/// </summary>
			public Stream Stream
			{
				get
				{
					return _stream;
				}
			}

			/// <summary>
			/// Gets or sets a context to be used by the serializers.
			/// </summary>
			public object Context { get; set; }

			/// <summary>
			/// Registers an item serializer.
			/// </summary>
			public void Register(IItemSerializer itemSerializer)
			{
				if (itemSerializer == null)
					throw new ArgumentNullException("itemSerializer");

				var forType = itemSerializer.ForType;
				if (forType == null)
					throw new ArgumentException("itemSerializer.ForType is null.", "itemSerializer");

				var reference = new ItemSerializerReference(_serializers.Count, itemSerializer);
				_serializers.Add(forType, reference);
				_types.Add(forType);
			}

			/// <summary>
			/// Verifies if the actual serializer has an item serializer registered for the given dataType.
			/// </summary>
			public bool CanSerialize(Type dataType)
			{
				return _serializers.ContainsKey(dataType);
			}

			/// <summary>
			/// Tries to get an item serializer.
			/// </summary>
			public ItemSerializerReference TryGetItemSerializer(Type dataType)
			{
				if (dataType == null)
					throw new ArgumentNullException("dataType");

				return _serializers.GetValueOrDefault(dataType);
			}

			/// <summary>
			/// Gets an item serializer for the given dataType or throws an exception.
			/// </summary>
			public ItemSerializerReference GetItemSerializer(Type dataType)
			{
				var result = TryGetItemSerializer(dataType);
				if (result.ItemSerializer == null)
					throw new ArgumentException("Can't find an ItemSerializer for " + dataType.FullName);

				return result;
			}
		#endregion
		#region Serialization
			private Dictionary<object, int> _alreadySerialized;

			/// <summary>
			/// Serializes a given item.
			/// </summary>
			public void Serialize(Stream stream, object item, Type deserializerWillExpectType=null)
			{
				if (stream == null)
					throw new ArgumentNullException("stream");

				if (_stream != null)
					throw new InvalidOperationException("This serializer is already in use. Maybe you want to call InnerSerialize.");

				try
				{
					_stream = stream;
					_alreadySerialized = new Dictionary<object, int>();
					_SerializeItem(item, deserializerWillExpectType);
				}
				finally
				{
					_alreadySerialized = null;
					_stream = null;
				}
			}

			/// <summary>
			/// During the process of serialization, a serializer may want to serialize another item.
			/// In that case InnerSerialize should be used.
			/// </summary>
			public void InnerSerialize(object item, Type deserializerWillExpectType=null)
			{
				if (_stream == null)
					throw new InvalidOperationException("You can only call InnerSerialize if you are inside a IItemSerializer.Serialize method.");

				_SerializeItem(item, deserializerWillExpectType);
			}

			private void _SerializeItem(object item, Type deserializerWillExpectType)
			{
				if (item == null)
				{
					OnSerializeNull();
					return;
				}

				var type = item.GetType();
				if (!type.IsValueType)
				{
					int id;
					if (_alreadySerialized.TryGetValue(item, out id))
					{
						OnSerializeReference(id);
						return;
					}
				}

				var itemSerializerReference = _serializers.GetValueOrDefault(type);
				var itemSerializer = itemSerializerReference.ItemSerializer;
				if (itemSerializer != null)
					OnSerializeType(itemSerializerReference.Id, type, deserializerWillExpectType);
				else
				{
					itemSerializer = GetItemSerializerForUnregisteredType(type, deserializerWillExpectType);
					if (itemSerializer == null)
						throw new SerializationException("Can't find the appropriate serializer for type " + type.FullName);
				}

				if (!type.IsValueType)
				{
					int id = _alreadySerialized.Count;
					_alreadySerialized.Add(item, id);
				}

				itemSerializer.Serialize(this, item);
			}

			/// <summary>
			/// Inheritors may implement this method to return a serializer for a type that is not registered.
			/// When that happens, OnSerializeType is not called. So it is up to you to do that.
			/// </summary>
			protected virtual IItemSerializer GetItemSerializerForUnregisteredType(Type type, Type deserializerWillExpectType)
			{
				return null;
			}

			/// <summary>
			/// Inheritors must implement to serialize a null value.
			/// </summary>
			protected abstract void OnSerializeNull();

			/// <summary>
			/// Inheritors must implement to serialize a reference to an already serialized item.
			/// </summary>
			protected abstract void OnSerializeReference(int id);

			/// <summary>
			/// Inheritors must implement to serialize the start of a new object of a given type
			/// being serialized.
			/// </summary>
			protected abstract void OnSerializeType(int typeIndex, Type type, Type deserializerWillExpectType);
		#endregion
		#region Deserialization
			internal Dictionary<int, object> _alreadyDeserialized;

			/// <summary>
			/// Deserializes an object.
			/// </summary>
			public object Deserialize(Stream stream, Type expectedType=null)
			{
				if (stream == null)
					throw new ArgumentNullException("stream");

				if (_stream != null)
					throw new InvalidOperationException("This deserializer is already in use. Are you trying to use an InnerDeserialize?");

				try
				{
					_idGenerator = 0;
					_stream = stream;
					_alreadyDeserialized = new Dictionary<int, object>();
					_referencesToFill = new List<KeyValuePair<int, Action<object>>>();

					object result = OnDeserialize(expectedType);
					_FillReferences();
					return result;
				}
				finally
				{
					_stream = null;
					_alreadyDeserialized = null;
					_referencesToFill = null;
				}
			}

			/// <summary>
			/// Fills a value when it becomes available (may not be immediate if it is a reference not yet finished).
			/// </summary>
			public void Fill(Action<object> action, Type expectedType=null)
			{
				if (action == null)
					throw new ArgumentNullException("action");

				var deserialized = InnerDeserialize(expectedType);
				var reference = deserialized as SerializationReference;
				if (reference == null)
					action(deserialized);
				else
					FillReference(reference.ReferenceId, action);
			}

			/// <summary>
			/// Use this if you need to deserialize a sub-object.
			/// </summary>
			public object InnerDeserialize(Type expectedType=null)
			{
				if (_stream == null)
					throw new InvalidOperationException("InnerDeserialize can only be called from a Deserialize method.");

				object result = OnDeserialize(expectedType);

				return result;
			}

			internal int _idGenerator;
			/// <summary>
			/// Method invoked when a deserialize needs to start. Should check for types, references, nulls etc.
			/// </summary>
			protected abstract object OnDeserialize(Type expectedType);

			/// <summary>
			/// Resolves a reference (gets the already deserialized object).
			/// </summary>
			protected object ResolveReference(int referenceId)
			{
				object possibleResult;
				if (_alreadyDeserialized.TryGetValue(referenceId, out possibleResult))
					return possibleResult;

				return new SerializationReference(referenceId);
			}

			private List<KeyValuePair<int, Action<object>>> _referencesToFill;
			/// <summary>
			/// Use this method to fill a reference when it is solved.
			/// </summary>
			public void FillReference(int id, Action<object> action)
			{
				if (id < 0)
					throw new ArgumentException("id can't be negative.", "id");

				if (action == null)
					throw new ArgumentNullException("action");

				_referencesToFill.Add(new KeyValuePair<int, Action<object>>(id, action));
			}

			private void _FillReferences()
			{
				int count = _alreadyDeserialized.Count;
				foreach(var pair in _referencesToFill)
				{
					var id = pair.Key;
					var action = pair.Value;

					if (id >= count)
						throw new SerializationException("An object was not found by its id.");

					var item = _alreadyDeserialized[id];
					action(item);
				}
			}

			private readonly List<Type> _types = new List<Type>();
			/// <summary>
			/// Gets a type by its id (determined by the order of Register calls).
			/// </summary>
			internal protected Type GetTypeById(int id)
			{
				return _types[id];
			}

			/// <summary>
			/// Creates an Id generator to determine the ids used to solve references.
			/// You must call this when the type is already known.
			/// </summary>
			protected DeserializationIdGenerator CreateIdGenerator(Type dataType)
			{
				if (dataType == null)
					throw new ArgumentNullException("dataType");

				return new DeserializationIdGenerator(this, dataType);
			}
		#endregion
	}
}
